什么是“异步调用”? “异步调用”对应的是“同步调用”,同步调用 指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用 指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。
我们在项目中经常会用到异步调用 ,比如在我们没有用到消息系统的情况下,用户注册时,需要发送注册成功的短信或邮件,再页面返回注册成功。发送短信邮件这一步,如果使用同步调用 ,有时候短信或邮件服务器网络不好,这一步可能会消耗一定的时间,用户需要长时间等待注册结果。而其实只有注册逻辑正确,我们可以认为用户就是注册成功了,可以直接返回结果而不需要等待短信邮件的发送结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service @Slf 4jpublic class UserService { @Autowired private UserMapper userMapper; public RespInfo<String> register (User user) throws Exception { int count = userMapper.insert(user); if (count == 1 ) { this .sendMessage(user); this .sendEmail(user); log.info("注册成功 user:{}" , user); return RespInfo.success("注册成功" ); } return RespInfo.error("注册失败" ); } private void sendMessage (User user) throws Exception { Thread.sleep(500 ); log.info("发送短信成功 user:{}" , user); } private void sendEmail (User user) throws Exception { Thread.sleep(1000 ); log.info("发送邮件成功 user:{}" , user); } }
我们用一个测试类调用一下看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RunWith (SpringRunner.class)@SpringBootTest (classes = DemoApplication.class)@Slf 4jpublic class Test1 { @Autowired private UserService userService; @Test public void testRegister () throws Exception { User user = new User(); user.setUsername("zhangsan2" ); user.setPassword("123456" ); userService.register(user); } }
看一下console输出
1 2 3 4 5 6 2018-04-13 17:01:57.866 INFO 950 --- [ main] Test1 : Started Test1 in 7.393 seconds (JVM running for 8.997) 2018-04-13 17:01:58.039 INFO 950 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2018-04-13 17:01:58.395 INFO 950 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2018-04-13 17:01:58.965 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 发送短信成功 user:User(id=3, username=zhangsan2, password=123456) 2018-04-13 17:01:59.969 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 发送邮件成功 user:User(id=3, username=zhangsan2, password=123456) 2018-04-13 17:01:59.970 INFO 950 --- [ main] com.tt.study.demo.service.UserService : 注册成功 user:User(id=3, username=zhangsan2, password=123456)
可以看出程序按顺序执行了发送短信、发送邮件、返回结果,短信和邮件耗时1.5s。
开启异步调用 在Spring Boot中,我们只需要通过使用@Async
注解就能简单的将原来的同步函数变为异步函数(注意函数需要public才能使注解生效,调用类和异步方法不能再同一个类里,否则异步失效),我们新建一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component @Slf 4jpublic class AsyncTask { @Async public void sendMessage (User user) throws Exception { Thread.sleep(500 ); log.info("发送短信成功 user:{}" , user); } @Async public void sendEmail (User user) throws Exception { Thread.sleep(1000 ); log.info("发送邮件成功 user:{}" , user); } }
修改下原来的调用类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service @Slf 4jpublic class UserService { @Autowired private UserMapper userMapper; @Autowired private AsyncTask asyncTask; public RespInfo<String> register (User user) throws Exception { int count = userMapper.insert(user); if (count == 1 ) { asyncTask.sendMessage(user); asyncTask.sendEmail(user); log.info("注册成功 user:{}" , user); return RespInfo.success("注册成功" ); } return RespInfo.error("注册失败" ); } }
为了让@Async注解能够生效,还需要在主程序DemoApplication中配置@EnableAsync,如下所示:
1 2 3 4 5 6 7 8 @SpringBootApplication ... @EnableAsync public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } }
运行一下测试类,console输出:
1 2 3 4 2018-04-13 18:06:39.624 INFO 1156 --- [nio-8080-exec-1] .s.a.AnnotationAsyncExecutionInterceptor : No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either 2018-04-13 18:06:39.626 INFO 1156 --- [nio-8080-exec-1] com.tt.study.demo.service.UserService : 注册成功 user:User(id=11, username=zhangsan7, password=123456) 2018-04-13 18:06:40.136 INFO 1156 --- [cTaskExecutor-1] c.tt.study.demo.service.async.AsyncTask : 发送短信成功 user:User(id=11, username=zhangsan7, password=123456) 2018-04-13 18:06:40.635 INFO 1156 --- [cTaskExecutor-2] c.tt.study.demo.service.async.AsyncTask : 发送邮件成功 user:User(id=11, username=zhangsan7, password=123456)
可以看到先返回了注册成功,发送短信和发送邮件在另外两个线程执行并输出。这样用户体验就更好了,程序运行时间也变短了。
使用自定义线程池 我们可以使用自定义线程池来控制异步调用,线程池的作用有:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。
定义线程池 我们新建一个TaskPoolConfig类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration @EnableAsync public class TaskPoolConfig { @Bean public Executor taskExecutor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10 ); executor.setMaxPoolSize(20 ); executor.setQueueCapacity(200 ); executor.setKeepAliveSeconds(60 ); executor.setThreadNamePrefix("taskExecutor-" ); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } }
使用线程池 在定义了线程池之后,我们如何让异步调用的执行任务使用这个线程池中的资源来运行呢?方法非常简单,我们只需要在@Async
注解中指定线程池名即可,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component @Slf 4jpublic class AsyncTask { @Async ("taskExecutor" ) public void sendMessage (User user) throws Exception { Thread.sleep(500 ); log.info("发送短信成功 user:{}" , user); } @Async ("taskExecutor" ) public void sendEmail (User user) throws Exception { Thread.sleep(1000 ); log.info("发送邮件成功 user:{}" , user); } }
测试一下 运行我们之前的测试类,查看console,发现有类似输出:
1 2 3 4 5 2018-04-15 13:17:25.255 INFO 2418 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'taskExecutor' ... 2018-04-15 13:17:29.541 INFO 2418 --- [ main] com.tt.study.demo.service.UserService : 注册成功 user:User(id=13, username=zhangsan10, password=123456) 2018-04-15 13:17:30.046 INFO 2418 --- [ taskExecutor-1] c.tt.study.demo.service.async.AsyncTask : 发送短信成功 user:User(id=13, username=zhangsan10, password=123456) 2018-04-15 13:17:30.550 INFO 2418 --- [ taskExecutor-2] c.tt.study.demo.service.async.AsyncTask : 发送邮件成功 user:User(id=13, username=zhangsan10, password=123456)
ok,我们用线程池管理异步调用完成了。
本节源码